import { KubernetesObject } from "@kubernetes/client-node"
import { K8SResourceType, KV, OperateFunction, PlanContextData } from "yam-types"
import { AppAlert } from "../types.d"

const DEFAULT_ALERT_INTERVAL = "8h"
const ALL_SEVERITY = ["warning", "error", "critical"]

let defaultRuleLabels = {
  "release": "prometheus"
}
const defaultAlertRoutingConfig = {
  groupBy: ['job'],
  groupWait: "10s",
  groupInterval: "2m",
  continue: true
}
const PROM_OPERATOR_API_GROUP = "monitoring.coreos.com"

const alertOperator: OperateFunction<AppAlert> = async (plan, diff, settings) => {
  defaultRuleLabels = Object.assign(defaultRuleLabels, settings.prometheusRuleLabels)

  // merge default settings into channel config, eg. { notification: { webhookConfig: { maxAlert: 10 }}}
  let defaultNotifications = {} as KV
  if (settings.notification) {
    defaultNotifications = Object.assign(defaultNotifications, settings.notification)
  }

  if (!diff.hasDiff) {
    plan.log.info(`no diff found for monitoring part, skipped`)
    return
  }
  const alerts = diff.currentItems.filter(x => x.enable !== false)
  if (alerts.length === 0 && diff.deletedItems.length === 0) {
    plan.log.info(`no content found for monitoring part, skipped`)
    return
  }

  plan.action(async (ctx) => {
    if (!ctx.k8sClient.crdExists(K8SResourceType.PrometheusRule, PROM_OPERATOR_API_GROUP)) {
      throw new Error("prometheus operator isn't installed")
    }
  }, "check-alert-crd:" + K8SResourceType.PrometheusRule)

  alerts.forEach(alert => {
    const { rule, config } = composeAlertRule(plan.data, alert, defaultNotifications)
    const ruleName = rule?.metadata?.name
    const configName = config?.metadata?.name
    plan.action(async (ctx) => {
      if (rule) {
        ctx.mergeToYaml({
          name: "prometheus-rule-" + ruleName,
          content: rule
        })
      }
      if (config) {
        ctx.mergeToYaml({
          name: "alertmanager-config-" + configName,
          content: config
        })
      }
      if (!ctx.k8sClient.crdExists(K8SResourceType.AlertmanagerConfig, PROM_OPERATOR_API_GROUP)) {
        ctx.log.error(`'${K8SResourceType.AlertmanagerConfig}' CRD not found, please install latest version of prometheus operator`)
      }
    }, "sync-alert-rule")
  })

  // when removing whole alert part
  diff.deletedItems.forEach(alert => {
    const { rule, config } = composeAlertRule(plan.data, alert, defaultNotifications)
    const ruleName = rule?.metadata?.name
    const configName = config?.metadata?.name
    plan.action(async (ctx) => {
      if (rule) {
        await ctx.k8sClient.delete({
          name: ruleName,
          kind: K8SResourceType.PrometheusRule
        })
      }
      if (config && ctx.k8sClient.crdExists(K8SResourceType.AlertmanagerConfig, PROM_OPERATOR_API_GROUP)) {
        await ctx.k8sClient.delete({
          name: configName,
          kind: K8SResourceType.AlertmanagerConfig
        })
      }
    }, "delete-alert-rule")
  })
}

const composeAlertRule = (planData: PlanContextData, alert: AppAlert, notifySettings: KV): { rule?: KubernetesObject, config?: KubernetesObject } => {
  return { rule: generateRule(planData, alert), config: generateChannelConfig(planData, alert, notifySettings) }
}

const generateRule = (planData: PlanContextData, alert: AppAlert): KubernetesObject | undefined => {
  if (!alert.rules || alert.rules.length === 0) {
    return
  }
  const alertRules: PrometheusAlertRule[] = alert.rules.map(r => {
    return {
      alert: r.name,
      annotations: {
        description: r.description || `Alert ${r.name} triggered`,
        summary: r.name
      },
      expr: r.condition,
      for: r.keep || '15s',
      labels: {
        job: planData.workloadName,
        namespace: planData.namespace,
        severity: r.severity
      }
    }
  }) || []
  return {
    kind: K8SResourceType.PrometheusRule,
    metadata: {
      name: planData.workloadName,
      labels: Object.assign({ workload: planData.workloadName }, defaultRuleLabels)
    },
    spec: {
      groups: [{
        name: planData.workloadName,
        rules: alertRules
      } as AlertGroup]
    }
  } as KubernetesObject

}

const generateChannelConfig = (planData: PlanContextData, alert: AppAlert, notifySettings: KV): KubernetesObject | undefined => {
  if (!alert.channels || alert.channels.length === 0) {
    return
  }
  alert.channels.forEach((c, idx) => {
    if (!c.name) {
      c.name = c.type + '-' + idx
    }
  })
  const workload = planData.workloadName
  const defaultChannel = alert.channels.filter(x => x.isDefault)[0]
  return {
    kind: K8SResourceType.AlertmanagerConfig,
    metadata: {
      name: planData.workloadName,
      labels: Object.assign({ workload }, defaultRuleLabels)
    },
    spec: {
      route: Object.assign({
        receiver: defaultChannel?.name,
        repeatInterval: defaultChannel?.repeatInterval || DEFAULT_ALERT_INTERVAL,
        matchers: [{
          name: "service",
          regex: false,
          value: workload
        }],
        routes: alert.channels.filter(x => !x.isDefault).map(c => {
          return {
            receiver: c.name,
            repeatInterval: c.repeatInterval || DEFAULT_ALERT_INTERVAL,
            matchers: [{
              name: "severity",
              regex: true,
              value: (c.matchSeverity || ALL_SEVERITY).join('|')
            }],
            continue: true
          }
        })
      }, defaultAlertRoutingConfig),
      receivers: alert.channels.map(c => {
        return {
          name: c.name,
          [c.type + "Config"]: Object.assign({}, c.config, notifySettings[c.type + 'Config'])
        }
      })
    }
  } as KubernetesObject
}

type AlertGroup = {
  name: string
  rules: PrometheusAlertRule[]
}

type PrometheusAlertRule = {
  alert: string,
  annotations: {
    description: string
    summary: string
  },
  expr: string
  for: string
  labels: {
    job: string
    namespace: string
    severity: string
  }
}

export default alertOperator
